# --coding:utf-8--

# Copyright 2021 xpstem.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import types
from enum import Enum, unique
from lelink.ev3type import *
from lelink.ev3encoder import *

@unique
class OpCode(Enum):
    """
    General operations
    """
    ERROR = 0x00
    NOP = 0x01
    PROGRAM_STOP = 0x02
    PROGRAM_START = 0x03
    OBJECT_STOP = 0x04
    OBJECT_START = 0x05
    OBJECT_TRIG = 0x06
    OBJECT_WAIT = 0x07
    RETURN = 0x08
    CALL = 0x09
    OBJECT_END = 0x0A
    SLEEP = 0x0B
    PROGRAM_INFO = 0x0C
    LABEL = 0x0D
    PROBE = 0x0E
    DO = 0x0F

    SYSTEM = 0x60
    PORT_CNV_OUTPUT = 0x61
    PORT_CNV_INPUT = 0x62
    NOTE_TO_FREQ = 0x63

    INFO = 0x7C
    STRING = 0x7D
    MEMORY_WRITE = 0X7E
    MEMORY_READ = 0x7F

    BP0 = 0x88
    BP1 = 0x89
    BP2 = 0x8A
    BP3 = 0x8B
    PB_SET = 0x8C
    RANDOM = 0x8E

    """
    User interface operations
    """
    UI_FLUSH = 0x80
    UI_READ = 0x81
    UI_WRITE = 0x82
    UI_BUTTON = 0x83
    UI_DRAW = 0x84

    KEEP_ALIVE = 0x90

    """
    Sound operations
    """
    SOUND = 0x94
    SOUND_TEST = 0x95
    SOUND_READY = 0x96

    """
    Input port operations
    """
    INPUT_DEVICELIST = 0x98
    INPUT_DEVICE = 0x99
    INPUT_READ = 0x9A
    INPUT_TEST = 0x9B
    INPUT_READY = 0x9C
    INPUT_READSI = 0x9D
    INPUT_READEXT = 0x9E
    INPUT_WRITE = 0x9F

    """
    Output port operations
    """
    OUTPUT_SETTYPE = 0xA1
    OUTPUT_RESET = 0xA2
    OUTPUT_STOP = 0xA3
    OUTPUT_POWER = 0xA4
    OUTPUT_SPEED = 0xA5
    OUTPUT_START = 0xA6
    OUTPUT_POLARITY = 0xA7
    OUTPUT_READ = 0xA8
    OUTPUT_TEST = 0xA9
    OUTPUT_READY = 0xAA
    OUTPUT_STEP_POWER = 0xAC
    OUTPUT_TIME_POWER = 0xAD
    OUTPUT_STEP_SPEED = 0xAE
    OUTPUT_TIME_SPEED = 0xAF

    OUTPUT_STEP_SYNC = 0xB0
    OUTPUT_TIME_SYNC = 0xB1
    OUTPUT_CLR_COUNT = 0xB2
    OUTPUT_GET_COUNT = 0xB3
    OUTPUT_PRG_STOP = 0xB4

    """
    Communication operations
    """
    COM_READY = 0xD0
    COM_TEST = 0xD1
    COM_GET = 0xD3
    COM_SET = 0xD4
    MAILBOX_OPEN = 0xD8
    MAILBOX_WRITE = 0xD9
    MAILBOX_READ = 0xDA
    MAILBOX_TEST = 0xDB
    MAILBOX_READY = 0xDC
    MAILBOX_CLOSE = 0xDD

    COM_READ = 0x91
    COM_WRITE = 0x92


def cal_gv_size(param_list: list) -> int:
    gv_size = 0
    for rp in param_list:
        gv_size = gv_size + rp.size()
    return gv_size


class EV3OperationPackage:
    __slots__ = ("op_code", "op_data", "gv_size", "lv_size")

    def __init__(self, op_code: OpCode, op_data: bytearray = None, gv_size: int = 0):
        self.op_code = op_code
        self.op_data = op_data
        self.gv_size = gv_size
        self.lv_size = 0

    def bytes(self) -> bytearray:
        ba = bytearray()
        ba.append(self.op_code.value)
        if self.op_data is not None:
            ba.extend(self.op_data)

        return ba


class ReturnParam:
    __slots__ = ("index",
                 "value",
                 "value_array")

    def __init__(self, index: int, value: EV3Type, value_array: list = None):
        self.index = index
        self.value = value
        self.value_array = value_array

    def size(self):
        if self.value_array is None:
            return self.value.size()
        else:
            return self.value.size() * len(self.value_array)

    def parse(self, ba: bytearray, index: int):
        if self.value_array is None:
            self.value.read_from(ba, index)
        else:
            while index > 0:
                ba.pop(0)
                index = index - 1
            for n in range(0, len(self.value_array)):
                self.value_array[n] = self.value.copy()
                self.value_array[n].read_from(ba, 0)


class EV3Operation:
    __slots__ = ("package",
                 "descr",
                 "returnparam_list")

    def __init__(self, package: EV3OperationPackage, returnparam_list: list = None, descr: str = None):
        self.package = package
        self.returnparam_list = returnparam_list
        self.descr = descr

    def description(self) -> str:
        if self.descr is None:
            return self.package.op_code.name

        return self.descr


class EV3CommOpFactory:

    CMD_GET_ON_OFF = 0x01
    CMD_GET_VISIBLE = 0x02
    CMD_GET_RESULT = 0x04
    CMD_GET_PIN = 0x05
    CMD_SEARCH_ITEMS = 0x08
    CMD_SEARCH_ITEM = 0x09
    CMD_FAVOUR_ITEMS = 0x0A
    CMD_FAVOUR_ITEM = 0x0B
    CMD_ID = 0x0C
    CMD_GET_BRICKNAME = 0x0D
    CMD_NETWORK = 0x0E
    CMD_PRESENT = 0x0F
    CMD_ENCRYPT = 0x10
    CMD_INCOMMING = 0x11
    CMD_SET_ON_OFF = 0x01
    CMD_SET_VISIBLE = 0x02
    CMD_SET_SEARCH = 0x03
    CMD_SET_PIN = 0x05
    CMD_SET_PASSKEY = 0x06
    CMD_SET_CONNECTION = 0x07
    CMD_SET_BRICKNAME = 0x08
    CMD_SET_MOVEUP = 0x09
    CMD_SET_MOVEDOWN = 0x0A
    CMD_SET_ENCRYPT = 0x0B
    CMD_SET_SSID = 0x0C

    __slots__ = ()

    @staticmethod
    def com_set_brickname(name: str) -> EV3Operation:
        ba = bytearray()
        LCEncoder(EV3CommOpFactory.CMD_SET_BRICKNAME).lc0(ba)
        LCEncoder(name).lcs(ba)

        op_pkg = EV3OperationPackage(OpCode.COM_SET, ba)
        return EV3Operation(op_pkg, None, "Com_Set_Brickname")

    @staticmethod
    def com_get_brickname(length: int, name: ReturnParam) -> EV3Operation:
        ba = bytearray()
        LCEncoder(EV3CommOpFactory.CMD_GET_BRICKNAME).lc0(ba)
        LCEncoder(length).lcx(ba)
        GVEncoder(name.index).gvx(ba)

        op_pkg = EV3OperationPackage(OpCode.COM_GET, ba, name.size)
        return EV3Operation(op_pkg, {name}, "Com_Get_Brickname")


class EV3GeneralOpFactory:
    __slots__ = ()

    @staticmethod
    def error() -> EV3Operation:
        op_pkg = EV3OperationPackage(OpCode.ERROR)
        return EV3Operation(op_pkg)

    @staticmethod
    def nop() -> EV3Operation:
        op_pkg = EV3OperationPackage(OpCode.NOP)
        return EV3Operation(op_pkg)



class EV3InputOpFactory:

    CMD_DEVICE_GET_TYPEMODE = 0x05
    CMD_DEVICE_READY_PCT = 0x1B
    CMD_DEVICE_READY_RAW = 0x1C
    CMD_DEVICE_READY_SI = 0x1D

    PORT_EMPTY_OR_UNAVAILABLE = 0x7E

    @staticmethod
    def device_list(length: Data8,
                    array_: ReturnParam,
                    changed: ReturnParam) -> EV3Operation:
        ba = bytearray()
        LCEncoder(length).lc0(ba)
        GVEncoder(array_.index).gvx(ba)
        GVEncoder(changed.index).gvx(ba)

        param_list = {array_, changed}
        gv_size = cal_gv_size(param_list)

        op_pkg = EV3OperationPackage(OpCode.INPUT_DEVICELIST, ba, gv_size)
        return EV3Operation(op_pkg, param_list)

    class OpInputDevice:
        @staticmethod
        def get_typemode(layer: Layer,
                         port: InputPort,
                         type_: ReturnParam,
                         mode: ReturnParam) -> EV3Operation:
            ba = bytearray()
            LCEncoder(EV3InputOpFactory.CMD_DEVICE_GET_TYPEMODE).lc0(ba)
            LCEncoder(layer).lc0(ba)
            LCEncoder(port).lc0(ba)
            GVEncoder(type_.index).gvx(ba)
            GVEncoder(mode.index).gvx(ba)

            param_list = {type_, mode}
            gv_size = cal_gv_size(param_list)

            op_pkg = EV3OperationPackage(OpCode.INPUT_DEVICE, ba, gv_size)
            return EV3Operation(op_pkg, param_list, "Input_Device_GetTypeMode")

        @staticmethod
        def ready_raw(layer: Layer,
                      port: InputPort,
                      type_: Data8,
                      mode: Data8,
                      values: Data8,
                      value_array: ReturnParam) -> EV3Operation:
            ba = bytearray()
            LCEncoder(EV3InputOpFactory.CMD_DEVICE_READY_RAW).lc0(ba)
            LCEncoder(layer).lc0(ba)
            LCEncoder(port).lc0(ba)
            LCEncoder(type_).lcx(ba)
            LCEncoder(mode).lcx(ba)
            LCEncoder(values).lcx(ba)
            GVEncoder(value_array.index).gvx(ba)

            op_pkg = EV3OperationPackage(OpCode.INPUT_DEVICE, ba, value_array.size)
            return EV3Operation(op_pkg, {value_array}, "Input_Device_ReadyRaw")

        @staticmethod
        def ready_pct(layer: Layer,
                      port: InputPort,
                      type_: Data8,
                      mode: Data8,
                      values: Data8,
                      value_array: ReturnParam) -> EV3Operation:
            ba = bytearray()
            LCEncoder(EV3InputOpFactory.CMD_DEVICE_READY_PCT).lc0(ba)
            LCEncoder(layer).lc0(ba)
            LCEncoder(port).lc0(ba)
            LCEncoder(type_).lcx(ba)
            LCEncoder(mode).lcx(ba)
            LCEncoder(values).lcx(ba)
            GVEncoder(value_array.index).gvx(ba)

            op_pkg = EV3OperationPackage(OpCode.INPUT_DEVICE, ba, value_array.size)
            return EV3Operation(op_pkg, {value_array}, "Input_Device_ReadyPct")

        @staticmethod
        def ready_si(layer: Layer,
                     port: InputPort,
                     type_: Data8,
                     mode: Data8,
                     values: Data8,
                     value_array: ReturnParam) -> EV3Operation:
            ba = bytearray()
            LCEncoder(EV3InputOpFactory.CMD_DEVICE_READY_SI).lc0(ba)
            LCEncoder(layer).lc0(ba)
            LCEncoder(port).lc0(ba)
            LCEncoder(type_).lcx(ba)
            LCEncoder(mode).lcx(ba)
            LCEncoder(values).lcx(ba)
            GVEncoder(value_array.index).gvx(ba)

            op_pkg = EV3OperationPackage(OpCode.INPUT_DEVICE, ba, value_array.size)
            return EV3Operation(op_pkg, {value_array}, "Input_Device_ReadySi")


class EV3OutputOpFactory:
    __slots__ = ()

    @staticmethod
    def output_start(layer: Layer, port_list: list) -> EV3Operation:
        ba = bytearray()
        LCEncoder(layer).lc0(ba)
        LCEncoder(port_list).lc0(ba)

        op_pkg = EV3OperationPackage(OpCode.OUTPUT_START, ba)
        return EV3Operation(op_pkg)

    @staticmethod
    def output_stop(layer: Layer, port_list: list, brake: Brake) -> EV3Operation:
        ba = bytearray()
        LCEncoder(layer).lc0(ba)
        LCEncoder(port_list).lc0(ba)
        LCEncoder(brake.value).lc0(ba)

        op_pkg = EV3OperationPackage(OpCode.OUTPUT_STOP, ba)
        return EV3Operation(op_pkg)

    @staticmethod
    def output_power(layer: Layer, port_list: list, power: int) -> EV3Operation:
        ba = bytearray()
        LCEncoder(layer).lc0(ba)
        LCEncoder(port_list).lc0(ba)
        LCEncoder(power).lcx(ba)

        op_pkg = EV3OperationPackage(OpCode.OUTPUT_PWOER, ba)
        return EV3Operation(op_pkg)

    @staticmethod
    def output_steppower(layer: Layer, port_list: list, power: int, step1: int, step2: int, step3: int, brake: Brake) -> EV3Operation:
        ba = bytearray()
        LCEncoder(layer).lc0(ba)
        LCEncoder(port_list).lc0(ba)
        LCEncoder(power).lcx(ba)
        LCEncoder(step1).lcx(ba)
        LCEncoder(step2).lcx(ba)
        LCEncoder(step3).lcx(ba)
        LCEncoder(brake.value).lcx(ba)

        op_pkg = EV3OperationPackage(OpCode.OUTPUT_STEP_POWER, ba)
        return EV3Operation(op_pkg)

    @staticmethod
    def output_timepower(layer: Layer, port_list: list, power: int, time1: int, time2: int, time3: int,
                         brake: Brake) -> EV3Operation:
        ba = bytearray()
        LCEncoder(layer).lc0(ba)
        LCEncoder(port_list).lc0(ba)
        LCEncoder(power).lcx(ba)
        LCEncoder(time1).lcx(ba)
        LCEncoder(time2).lcx(ba)
        LCEncoder(time3).lcx(ba)
        LCEncoder(brake.value).lcx(ba)

        op_pkg = EV3OperationPackage(OpCode.OUTPUT_TIME_POWER, ba)
        return EV3Operation(op_pkg)